Javaのクラスファイルをjavapとバイナリエディタで読む
はじめに
こんにちは、虎塚です。
この記事はJava Advent Calendar 2014 の22日目の記事です。昨日はすふぃあ (@empressia) さんの「JavaEEなWebアプリケーションを作ろうとしたときのお話: すふぃあの記憶」でした。
この記事では、「Javaクラスファイルの読み方・増補版」と題しまして、12月20日(土)に開催したJavaクラスファイル入門という勉強会でお話しした内容の補足をお届けします。なお、勉強会のターゲットは、
- Javaプログラムは書いたことがあるけど、JVMのことは全然知らない
- Javaクラスファイルのバイナリを見たことがない
といった初心者の方や新人さんでした。なので、Javaに興味さえあれば、どなたでもお読みいただける内容かと思います。
JVM仕様とは
JavaとJVM
Javaプログラム(.java)をコンパイルすると、中間コードと呼ばれるJavaクラスファイル(.class)が生成されます。
Javaプログラムが、OSやハードウェアなどが異なるプラットフォームでも実行できるのは、中間コードのおかげです。
JVMの内部に、中間コードを特定のプラットフォームで動かすために必要なプラットフォーム依存の機能があり、中間コードを読み込んで決められた振る舞いをするように処理してくれます。
JVM仕様とは何か
JVM仕様を一言でいうと、「入力であるクラスファイルのデータ構造と、出力である振る舞いのルールを定義するもの」です。
このルールを満たした実装が、JVMと呼ばれます。逆に言うと、JVM仕様とは、実在する特定のJVMプロダクトの仕様を定めたものではありません。
JVM仕様では、上記定義を実現するための実装の詳細については、踏み込みません。
そして、JVM仕様の一部であるクラスファイルのデータ構造、つまりclassファイルフォーマットとは、Javaのクラスファイルを表現するためのバイナリの並びを定義したものです。
クラスファイルを見るツール
Javaのクラスファイルを見るためのツールとして、逆アセンブルツールとバイナリエディタがあります。
- Javaの逆アセンブルツール
- バイナリデータをJavaのclassファイルとして解釈し,JVM仕様で定義されたデータ構造との対応を表示する
- 入力として想定するのは、Javaのクラスファイルだけ
- バイナリエディタ
- バイナリエディタを読みやすく整形して表示する
- 入力として想定するのは、ファイルなら何でも
この記事では、逆アセンブルのツールとして、javapを扱います。
javap
javapとは、OpenJDKベースのJDKに付属している標準ツールです。コマンドラインから利用します。
javapを使う場面としては、次のようなケースが考えられます。
- 複数のコンパイラを使う状況で,コンパイラごとに出力される中間コードの差分を見たいとき
- 処理系を作る際に,期待する入力と実際の中間コードの差分を見たいとき
要は、バイナリとは異なる粒度でJavaクラスファイル間の差分を見たい時に使うと思います。
クラスファイルをjavapで見る
たとえば、「hello」と出力するJavaプログラムHello.javaを書いて、コンパイルします。
% vi Hello.java class Hello{ public static void main(String[] args){ System.out.pintln(“hello”); } } % javac Hello.java
このクラスファイルをjavapの引数に渡して実行します。情報量を増やすため、「-v」オプションをつけましょう。
次のような出力が得られました。
% javap -v Hello.class Classfile /Users/torigatayuki/src/javap-sample/1220/Hello.class Last modified 2014/12/20; size 409 bytes MD5 checksum 786366c9c8962af2a9d3e1cf3284d69c Compiled from "Hello.java" class Hello SourceFile: "Hello.java" minor version: 0 major version: 52 flags: ACC_SUPER Constant pool: #1 = Methodref #6.#15 // java/lang/Object."<init>":()V #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #18 // hello #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #21 // Hello #6 = Class #22 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 Hello.java #15 = NameAndType #7:#8 // "<init>":()V #16 = Class #23 // java/lang/System #17 = NameAndType #24:#25 // out:Ljava/io/PrintStream; #18 = Utf8 hello #19 = Class #26 // java/io/PrintStream #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V #21 = Utf8 Hello #22 = Utf8 java/lang/Object #23 = Utf8 java/lang/System #24 = Utf8 out #25 = Utf8 Ljava/io/PrintStream; #26 = Utf8 java/io/PrintStream #27 = Utf8 println #28 = Utf8 (Ljava/lang/String;)V { Hello(); descriptor: ()V flags: Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String hello 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 3: 0 line 4: 8 }
バイナリエディタ
バイナリエディタは、GUIツールもコマンドラインツールも非常に豊富な種類があります。
この記事では、Mac上で0xEDを使います。
# 勉強会当日に、コマンドラインツールのhexdumpも便利だと教えていただきました。こちらはMacやLinuxで使えます。Windowsの方には、BZeditがおすすめです。
クラスファイルをバイナリエディタで見る
バイナリエディタでクラスファイルを開くだけです。0xEDの場合、次のように表示されます。
クラスファイルを読んでみよう
それでは、Javaプログラムのクラスファイルを読む手順を説明します。javap、バイナリエディタ、そしてJVM仕様書を参照します。
最新のJVM仕様書は、次のページで確認できます。
ClassFile
ClassFileは、JVM仕様書の「4.1 The ClassFile Structure」を参照すると、次のような構造になっています。
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
クラス、あるいはインタフェース1つに対して、この構造が1つ存在します。
u4、u2といった値は、型を表わします。u4は、符号なし4バイトの型です。magic、minor_versionといった値は、項目です。
これらの項目を読む方法をウォークスルーで説明します。
magic
ClassFile構造の最初の項目は、magicです。
これは、Javaクラスファイルを識別するための定数値「CAFEBABE」です。Javaのクラスファイルには、先頭に必ずこのバイナリ列がくる決まりになっています。
CAFEBABEは16進数の表現です。magicはu4型なので、4バイトです。実際のバイナリは「11001010111111101011101010111110」ですね。
CAFEBABEという文字列の由来は、James Gosling氏によると次のようなエピソードがあるそうです。興味のある方はどうぞ。
javapで見る
javapの出力項目にmagicはありません。そもそも、javapの入力はJavaクラスファイルだけのはずだからですね。
magicが異なるファイルを読み込むと、ClassFormatErrorが発生します。OpenJDKのjavapの実装では、次のようになっています。
magic = in.readInt(); if (magic != JAVA_MAGIC) { throw new ClassFormatError("wrong magic: " + toHex(magic) + ", expected " + toHex(JAVA_MAGIC));
バイナリエディタで見る
先頭が「CAFEBABE」となっていることが確認できます。
minor_version、major_version
次は、minor_version、major_versionです。これらは、classファイルフォーマットのバージョンを表わします。minor_versionとmajor_version、2項目合わせてバージョンを特定します。
javapで見る
javap出力内容の次の箇所が、minor_version、major_versionです。
minor version: 0 major version: 52
バイナリエディタで見る
バイナリエディタでは、次のように見えます。
major_versionが34となっていますが、これは16進数です。10進数に変換すると52なのでで、javapの出力結果と一致しますね。
constant_pool_count
constant_pool_countは、続くconstant_poolテーブルのエントリ数です。正確には、constant_poolのエントリ数は、この値よりも1少なくなります。これについては、後ほど説明します。
バイナリエディタで見る
バイナリエディタでは、次のように見えます。
16進数で1Dとあるので、10進数に直すと29。つまり、constant_poolには、29-1=28個のエントリがあると分かります。
constant_pool[constant_pool_count-1]
constan_poolは、可変長のテーブルです。上記のconstant_pool_countによって、要素の数は分かりますが、中身の構成を見るまで、何バイトあるかは分かりません。
なぜなら、constant_pool要素の型であるcp_infoには、複数の種類があるからです。JVM仕様書の「4.4 The Constant Pool」によると、cp_infoは次のような構造をしています。
cp_info { u1 tag; u1 info[]; }
1つ目の項目である「tag」の種類によって、cp_infoの種類(Constant Type)が特定されます。JVM仕様書の「Table 4.4-A. Constant pool tags」には、14種類のConstant Typeが列挙されています。
javapで見る
constant_poolは、javap出力内容の次の箇所です。Methodref、Fieldref、String、……といった項目が、Constant Typeに当たります。
Constant pool: #1 = Methodref #6.#15 // java/lang/Object."<init>":()V #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #18 // hello #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #21 // Hello #6 = Class #22 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 Hello.java #15 = NameAndType #7:#8 // "<init>":()V #16 = Class #23 // java/lang/System #17 = NameAndType #24:#25 // out:Ljava/io/PrintStream; #18 = Utf8 hello #19 = Class #26 // java/io/PrintStream #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V #21 = Utf8 Hello #22 = Utf8 java/lang/Object #23 = Utf8 java/lang/System #24 = Utf8 out #25 = Utf8 Ljava/io/PrintStream; #26 = Utf8 java/io/PrintStream #27 = Utf8 println #28 = Utf8 (Ljava/lang/String;)V
左端のインデックスが1から28まであり、constant_pool_countの数より1つ少ないことが確認できます。
じつは、インデックスは本当は0から始まります。しかし、出力には「#0」がありません。
これらのインデックス番号は、ClassFile構造のconstant_pool以外の場所から参照されます。ところが、0という数値は「データが存在しない」ことを意味します。そのため、0をインデックスとして使わないように欠番になっているのだそうです。
バイナリエディタで見る
constant_poolの1つ目のエントリ(javap出力の#1に当たる要素)を確認します。
まず、最初の1バイト「tag」を見ると、「0A」です。10進数では10にあたります。JVM仕様書のTable 4.4-A. Constant pool tagsを参照すると、tag「10」のcp_infoはCONSTANT_Methodrefだと分かります。
次に、JVM仕様書でCONSTANT_Methodref_infoの構造を調べると、次のように書いてあります。
CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
「tag」は、先ほど読んだ「0A」のことです。続いて、2バイトの要素が2つあります。CONSTANT_Methodref_infoは、全体で5バイトの構造であることが分かりました。
これを図示したのが、上のスクリーンショットです。このように、バイナリとJVM仕様書を参照する作業を、constant_poolテーブルのエントリの数だけ繰り返します。
constant_poolの28個の要素を図示すると、次のようになります。
枠の色が黒の要素は、tag01、つまりStringの要素を示しています。バイナリ列の右側に表示された文字と見比べてみてください。
access_flags
access_flagsは、アクセス許可やクラスの属性を表わすu2型の項目です。フラグの種別は、最新のJVM仕様では8種類あります。
Javaがバージョン1.2の頃には5種類でしたが、アノテーションやEnumを表わすフラグが後から追加されました。
javapで見る
javapの出力結果にも、「flags」という項目名で表示されていました。
flags: ACC_SUPER
javapで見る
バイナリエディタで見ると、次の部分がaccess_flagsです。
16進数で0020が表示されています。JVM仕様書の「Table 4.1-A. Class access and property modifiers」を参照すると、0020はACC_SUPERを意味することが分かります。
this_class、super_class
this_classとsuper_classは、先ほど登場したconstant_poolテーブルへの参照値です。
その名のとおり、this_classはそのクラス自身、super_classは直接のsuper classの名前を指します。なお、Objectクラスの場合はsuper classが存在しないため、参照ではなく値0が入ります。
バイナリエディタで見る
この項目は、javap出力内容より先にバイナリで見た方が分かりやすいかと思います。
- this_class: 16進数の「5」、つまりconstant_poolの5番目の要素への参照です
- constant_poolの5番目の要素: 16進数の「15」、つまりconstant_poolの21番目の要素への参照です
- constant_poolの21番目の要素: 16進数の「48656C6C6F」。これは文字列「Hello」で、クラス名です。
this_classの参照をたどると、クラス名の文字列に行き着きました。
javapで見る
javapの出力で、constant_pool内の該当する要素を見てみましょう。
#5 = Class #21 // Hello (略) #21 = Utf8 Hello
参照先要素のインデックス(と値)が、右側に書かれていますね。
interfaces_count、interfaces[interfaces_count]
interfaces_countは、このクラス、あるいはインタフェースの直接のsuper interfaceの数です。
また、interfaces[interfaces_count]は、CONSTANT_Class_info要素を持つ配列です。interfacesは、constant_poolのような可変長のテーブルではなく、固定長の配列です。
バイナリエディタで見る
バイナリエディタでは、(2行に渡っていてちょっと見づらいですが)次のようになります。
「0」ですね。Helloクラスはインタフェースを実装していないため、interfaces_countは0になります。
interfaces_countが0の場合、interfaces配列は存在しないことに注意してください。バイナリ列でも、項目丸ごと飛ばされます。つまり、interfaces_countの次には、interfaces配列のさらに次の項目が続くことになります。
fields_count、fields[fields_count]
fields_countは、このクラス,あるいはインタフェースで定義されているフィールドの数です。
また、fields[fields_count]は、field_info構造を持つ可変長のテーブルです。
field_infoは、JVM仕様書の「4.5 Fields」によると、次のような構造をしています。
field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
フィールドにもaccess_flagsがありますね。これは、先ほど登場したクラス用のaccess_flagsとは別物のフィールド用のaccess_flagsです。
attribute_infoについては後述します。
今回のHelloクラスでは、fields_countも(先ほどのinterfaces_countと同じく)「0」なので、fieldsテーブルは存在しません。次の項目へ進みましょう。
methods_count、methods[methods_count]
methods_countは、このクラス,あるいはインタフェースで定義されているメソッドの数です。
また、methods[methods_count]は、method_info構造を持つ可変長のテーブルです。
method_infoは、JVM仕様書の「4.6 Methods」によると、次のような構造をしています。
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
fields_infoの構造とそっくりですね。ここでも、メソッド専用のaccess_flagsが先頭に配置されています。
バイナリエディタで見る
バイナリエディタで、methods_countに当たる部分を見てみましょう。
「2」になっています。Helloクラスにはmainメソッドしか定義しなかったのに、なぜメソッド数が2になるのでしょうか。それは、Helloクラスのデフォルトコンストラクタがカウントされているからです。
前掲のHelloクラスのサンプルコードでは、コンストラクタを定義していませんでした。そのため、コンパイル時にデフォルトコンストラクタが自動生成されました。
クラス全体でのメソッド数は、デフォルトコンストラクタとmainメソッドの2個になります。
attributes_count、attributes[attributes_count]
ついに最後の項目です。
attributes_countは、JVM仕様を見ても、attribute_info構造の数としか書かれていません。続く項目であるattributes[attributes_count]の要素数を表わします。
では、attribute_infoとは何でしょうか。JVM仕様書の「4.7 Attributes」によると、次のような構造をしています。
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
attribute_infoは、attribute_name_indexの種類によって構造の種類が変わります。constant_poolの要素が、tagによって異なる構造を持っていたのと似ていますね。現時点では、23種類のattributeが定義されています。(「Table 4.7-A. Predefined class file attributes」を参照)
attribute_infoは、ClassFile構造の中に繰り返し登場します。ClassFile構造の末尾の項目でありつつ、その手前のfieldsテーブルやmethodsテーブルの内部にも現れます。さらに、Attributeの構造の1つであるCode_attributeの中に、入れ子の形で登場することもあります。
また、attributeの面白いところは、定義済み以外の値を新規に追加できることです。つまり、Javaの言語機能に何か追加があった時、何か増えるならこの部分ということです。実際、Java 8では、タイプアノテーションが言語機能に追加されたため、Attributeに新しくRuntimeVisibleTypeAnnotationsとRuntimeInvisibleTypeAnnotationsが追加されました。
バイナリエディタで見る
上記の構造を踏まえて、methodsテーブル内のメソッド構造を1個確認してみましょう。Attributesが含まれています。
まず、method_info構造の前から2バイト×4個は、 access_flags、name_index、descriptor_index、attributes_countで固定長です(図中の枠がピンク色の部分)。
次に、attribute_infoの本体です。こちらも前から2バイト、4バイトは、attribute_name_index、attribute_lengthで固定長です(図中の枠が水色の部分)。
attribute_name_indexの値に注目してください。これは、constant_poolテーブルのインデックスを参照しています。ここでは「09」なので、constant_poolの9番目の要素を見ると、「Code」という文字列です。つまり、このattribute_infoは、Attributeの1つであるCode_attributeであることが分かります。
Code_attributeは、JVM仕様書の「4.7.3 The Code attribute」によると、次のような構造になっています。
Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attributes_count]; }
attribute_name_indexとattribute_lengthまでは、attribute_infoすべてに共通で、すでに確認した部分です。続く2バイト×2と4バイトが、max_stack、max_locals、code_lengthで固定長です。code_lengthの値は「5」です。続くcode配列に5個の要素があることが分かります。
code[code_length]の部分は、次のようになっています。
2A B7 00 01 B1
この意味は、JVM仕様書の「Chapter 7. Opcode Mnemonics by Opcode」を参照すると確認できます。これらのバイナリと対応するニーモニックを抜粋すると、次のようになります。
- (0x2a) aload_0
- (0xb7) invokespecial
- (0x00) nop
- (0x01) aconst_null
- (0xb1) return
これは、javap出力内容の次の部分に対応しています。
0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return
さて、classファイルのバイナリを読むために必要な最小限の流れは、ここまでで記述できたかと思います。そろそろおしまいにしますが、後続の要素をあともう少しだけ見てみましょう。
code[code_length]の次には、exception_table_lengthという2バイトの項目があります。この値が「0」なので、その後のexception_table[exception_table_length]は飛ばされて、attributes_count項目が続きます(図中の枠が黄緑色の部分)。attributeの入れ子が現れました。
ここからは、先ほどCode_attributeを特定した時と同様の作業です。attribute_name_indexの値を見て、参照先のconstant_poolの10番目の要素を確認すると、「LineNumberTable」という文字列です(図中の枠がオレンジの部分)。
Code_attributeの中に入れ子になっているattribute_infoは、LineNumberTable_attributeであることが分かりました。
続きが気になる方は、JVM仕様書を参照しながら、Hello.classを末尾まで読んでみてください。
おわりに
JVM仕様書を参照しながら、javapとバイナリエディタでJavaのクラスファイルを読む方法を説明しました。構造が入れ子になっていたり、参照があったりするため、仕様書のあちこちを飛び回ることになりますが、すべての構造が仕様書に書かれているので、ゆっくり読めば必ず読めます。
今年のクリスマスはジングルベルを聴きながら、普段実行しているJavaのクラスファイルをバイナリエディタで鑑賞してみるのも楽しいかもしれませんね。
Java Advent Calendar 2014、明日は@nagaseyasuhitoさんの予定です。
それでは、また。